//! Project configuration for handling path aliases and module resolution

use crate::error::{CompilerError, Result};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs;
use std::path::{Path, PathBuf};

/// Project configuration (nrc.json)
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ProjectConfig {
    /// Project name
    pub name: Option<String>,
    /// Project version
    pub version: Option<String>,
    /// Project root directory (relative to config file)
    #[serde(default = "default_root")]
    pub root: String,
    /// Path aliases (@project, @lib, @std, etc.)
    #[serde(default)]
    pub paths: HashMap<String, String>,
}

fn default_root() -> String {
    ".".to_string()
}

impl Default for ProjectConfig {
    fn default() -> Self {
        Self {
            name: None,
            version: None,
            root: ".".to_string(),
            paths: HashMap::new(),
        }
    }
}

impl ProjectConfig {
    /// Load project configuration from nrc.json
    pub fn load_from_dir(dir: &Path) -> Result<(Self, PathBuf)> {
        // Try to find nrc.json in current dir or parent dirs
        let config_path = Self::find_config_file(dir)?;
        let config_dir = config_path
            .parent()
            .ok_or_else(|| CompilerError::internal("Failed to get config directory"))?;

        // Read and parse config file
        let content = fs::read_to_string(&config_path)
            .map_err(|e| CompilerError::internal(&format!("Failed to read nrc.json: {}", e)))?;

        let config: ProjectConfig = serde_json::from_str(&content)
            .map_err(|e| CompilerError::internal(&format!("Failed to parse nrc.json: {}", e)))?;

        Ok((config, config_dir.to_path_buf()))
    }

    /// Find nrc.json by searching upwards from the given directory
    fn find_config_file(start_dir: &Path) -> Result<PathBuf> {
        let mut current = start_dir.to_path_buf();

        loop {
            let config_path = current.join("nrc.json");
            if config_path.exists() && config_path.is_file() {
                return Ok(config_path);
            }

            // Try to go up one level
            if let Some(parent) = current.parent() {
                current = parent.to_path_buf();
            } else {
                // Reached root, not found
                break;
            }
        }

        // Fall back to trying to detect project root by other means
        Self::auto_detect_project_root(start_dir)
    }

    /// Auto-detect project root by looking for indicators
    fn auto_detect_project_root(start_dir: &Path) -> Result<PathBuf> {
        let mut current = start_dir.to_path_buf();

        loop {
            // Check for various project root indicators
            let indicators = vec![
                ".git",       // Git repository root
                "Cargo.toml", // Rust project (for now during development)
                "src",        // Common source directory
                "main.nr",    // Main entry point
            ];

            for indicator in &indicators {
                let indicator_path = current.join(indicator);
                if indicator_path.exists() {
                    // Create a default nrc.json path (even if it doesn't exist)
                    return Ok(current.join("nrc.json"));
                }
            }

            // Try to go up one level
            if let Some(parent) = current.parent() {
                current = parent.to_path_buf();
            } else {
                // Reached root, use current directory
                return Ok(start_dir.join("nrc.json"));
            }
        }
    }

    /// Create a default configuration with standard paths
    pub fn create_default(project_root: &Path) -> Self {
        let mut paths = HashMap::new();

        // Add default path aliases
        paths.insert("@project".to_string(), ".".to_string());
        paths.insert("@src".to_string(), "./src".to_string());

        // Try to find std directory
        if let Ok(std_path) = Self::find_std_directory() {
            if let Ok(relative) = std_path.strip_prefix(project_root) {
                paths.insert("@std".to_string(), format!("./{}", relative.display()));
            } else {
                // Use absolute path if std is outside project
                paths.insert("@std".to_string(), std_path.to_string_lossy().to_string());
            }
        }

        Self {
            name: None,
            version: None,
            root: ".".to_string(),
            paths,
        }
    }

    /// Resolve a path alias to actual path
    pub fn resolve_alias(&self, alias: &str, config_dir: &Path) -> Option<PathBuf> {
        self.paths.get(alias).map(|path| {
            let resolved = config_dir.join(&self.root).join(path);
            resolved.canonicalize().unwrap_or(resolved)
        })
    }

    /// Find the std directory
    fn find_std_directory() -> Result<PathBuf> {
        // Try to find the std directory relative to the compiler executable
        let exe_path = std::env::current_exe().map_err(|e| {
            CompilerError::internal(&format!("Failed to get current executable path: {}", e))
        })?;

        let exe_dir = exe_path
            .parent()
            .ok_or_else(|| CompilerError::internal("Failed to get executable directory"))?;

        // Look for std directory in several possible locations
        let possible_paths = vec![
            exe_dir.join("std"),
            exe_dir.join("../std"),
            exe_dir.join("../../std"),
            exe_dir.join("../../../std"),
        ];

        for path in possible_paths {
            if path.exists() && path.is_dir() {
                return Ok(path.canonicalize().unwrap_or(path));
            }
        }

        // Try relative to current working directory
        let cwd_std = std::env::current_dir()
            .unwrap_or_else(|_| PathBuf::from("."))
            .join("std");

        if cwd_std.exists() && cwd_std.is_dir() {
            return Ok(cwd_std.canonicalize().unwrap_or(cwd_std));
        }

        Err(CompilerError::internal("Could not find std directory"))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_default_config() {
        let config = ProjectConfig::default();
        assert_eq!(config.root, ".");
        assert!(config.name.is_none());
    }

    #[test]
    fn test_create_default() {
        let project_root = PathBuf::from("/tmp/test_project");
        let config = ProjectConfig::create_default(&project_root);

        assert!(config.paths.contains_key("@project"));
        assert!(config.paths.contains_key("@src"));
    }
}
